/*
 *  Cero Project - Copyright   2006 The Cero Developement Team 
 *  (Michael Laguerre, Camille Roux, Matthieu Segret, Mathieu Sivade) 
 * 
 *  This program is free software; you can redistribute it and/or modify it 
 *  under the terms of the GNU General Public License as published by the Free 
 *  Software Foundation; either version 2 of the License, or (at your option) 
 *  any later version.
 * 
 *  This program is distributed in the hope that it will be useful, but 
 *  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 *  or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
 *  for more details.
 */

package org.ceroproject.plugin;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.ceroproject.UnsupportedPluginException;
import org.ceroproject.games.AIPlayer;
import org.ceroproject.games.Game;
import org.ceroproject.games.GameInitializer;
import org.ceroproject.games.Player;
import org.ceroproject.games.Rule;
import org.ceroproject.ui.InterfaceModule;
import org.ceroproject.ui.UserInterface;


/**
 * This class gives you acces to all the plugins you might want to use. It is a
 * singleton, and you must *NOT* try to instanciate it, as it will throw an
 * Exception if you do. Use getInstance instead to get a reference on the
 * PluginManager.
 * 
 * @author Telem
 */
public class PluginManager {
	/** The collection of user interfaces */
	private Collection<InterfacePlugin> uiPlugins = new ArrayList<InterfacePlugin>();

	/** The collection of modules designed for user interfaces */
	private Collection<InterfaceModulePlugin> uiPluginModules = new ArrayList<InterfaceModulePlugin>();

	/** The collection of games mapped by their name */
	private Map<String, GamePlugin> gamePlugins = new HashMap<String, GamePlugin>();

	/** The class loaders mapped by the folder they loaded */
	private Map<File, ClassLoader> classLoaders = new HashMap<File, ClassLoader>();

	// FIXME : à faire disparaitre
	/**
	 * The collection of the UI instances used, currently needed by PlayerBase
	 * to return a ChoiceMaker
	 */
	private Collection<UserInterface> uiInstances = new ArrayList<UserInterface>();

	/** The unique instance of this class */
	private static PluginManager instance = new PluginManager();

	private PluginManager() {
	}

	public static PluginManager getInstance() {
		return instance;
	}

	/**
	 * Loads the folder specified but no subfolder
	 * 
	 * @param folder
	 *            the folder to load
	 */
	public void loadFolder(String folder) {
		loadFolder(folder, 0);
	}

	/**
	 * Loads the folder 'folder' and its subfolders up to a deepness of
	 * 'recDeepeness'. If recDeepness is negative, all subfolders are loaded.
	 * 
	 * @param folder
	 *            The folder to load
	 * @param recDeepness
	 *            The deepness up to which subfolders should be loaded
	 */
	public void loadFolder(String folder, int recDeepness) {
		if (recDeepness < 0)
			recDeepness = -1;

		File file = new File(folder);
		if (!file.isDirectory())
			throw new IllegalArgumentException("Specified file (" + folder
					+ ") is not a folder");

		// looking if this folder's parent has been loaded
		ClassLoader parentcl = classLoaders.get(file.getParentFile());

		DynamicURLClassLoader durlcl;
		if (parentcl != null)
			durlcl = new DynamicURLClassLoader(new URL[0], parentcl);
		else
			durlcl = new DynamicURLClassLoader(new URL[0]);

		if (classLoaders.get(file) != null) {
			// this folder hasn't been loaded yet
			classLoaders.put(file, durlcl);
		}

		// Extracting the list of files contained in the path
		String[] filesToLoad = file.list();

		// Preparing a directory collection to load after the files
		Collection<File> directories = new ArrayList<File>();
		Set<Class<?>> loadedClasses = new HashSet<Class<?>>();
		File tmpfile;
		for (int i = 0; i < filesToLoad.length; i++) {
			// getting the current file
			tmpfile = new File(folder + "/" + filesToLoad[i]);
			if (tmpfile.isDirectory())
				directories.add(tmpfile);
			else if (filesToLoad[i].endsWith(".jar")) {
				// if it is a jar file, loads each plugin contained in it
				// adds the URL so that the ClassLoader can find it
				Set<String> loadedClassesNames;
				try {
					durlcl.addURL(tmpfile.toURL());
					// lists the classes to load
					loadedClassesNames = listClassesInJar(tmpfile);
				} catch (IOException e) {
					// The file couldn't be accessed
					e.printStackTrace();
					continue;
				}
				// loads the class
				for (String name : loadedClassesNames) {
					try {
						loadedClasses.add(durlcl.loadClass(name));
					} catch (ClassNotFoundException e1) {
						// There was an invalid .class file in the jar archive
						e1.printStackTrace();
					}
				}

			}
		}

		// search and load plugins
		Collection<Plugin> pluginsFound = new ArrayList<Plugin>();
		GamePlugin gp;
		try {
			gp = loadGamePlugin(loadedClasses);
			if (gp != null)
				pluginsFound.add(gp);
		} catch (BrokenPluginException e) {
			// TODO the user must get aware of this somehow
			e.printStackTrace();
		}

		pluginsFound.addAll(loadInterfacePlugins(loadedClasses));
		pluginsFound.addAll(loadInterfaceModulePlugins(loadedClasses));

		for (Plugin p : pluginsFound) {
			if (p instanceof GamePlugin)
				System.out.print("Game");
			else if (p instanceof InterfacePlugin)
				System.out.print("Game Interface");
			else if (p instanceof InterfaceModulePlugin)
				System.out.print("Game Interface Module");
			else
				System.out.print("Something named");
			System.out.println(" \"" + p.getPluginName() + "\" was loaded");
		}

		// once the files of the folder are loaded, we load the subfolders
		// if needed
		if (recDeepness != 0)
			for (File f : directories)
				loadFolder(f.getAbsolutePath(), recDeepness - 1);

	}

	/**
	 * Lists the classes corresponding to the .class files contained in the jar.
	 * 
	 * @param jarfile
	 *            The file to analyse
	 * @return The list of the class names
	 * @throws IOException
	 *             if the jar couldn't be accessed
	 */
	private static Set<String> listClassesInJar(File jarfile)
			throws IOException {
		Set<String> classes = new HashSet<String>();
		JarFile jf = new JarFile(jarfile);
		Enumeration<JarEntry> jarEntries = jf.entries();
		while (jarEntries.hasMoreElements()) {
			String entryname = jarEntries.nextElement().getName();
			if (entryname.endsWith(".class"))
				classes.add(entryname.replace("/", ".").substring(0,
						entryname.length() - 6));
			/*
			 * XXX : est-ce nécessaire ? else if (entryname.endsWith(".alias"))
			 * classes.add(entryname.replace("/", ".").substring(0,
			 * entryname.length() - 6));
			 */
		}
		return classes;
	}

	/**
	 * Checks whether the Class c could be instantiable using newInstance().
	 * This method returns true if c is public, not an interface, not abstract,
	 * not local, not a member class, not an annotation, not anonymous. Though
	 * some of these conditions aren't linked to being instantiable, there is no
	 * mean in instantiating an unknown class in our context. Note this function
	 * does not every check needed : public constructors aren't checked either.
	 * 
	 * @param c
	 *            The class which instantiability must be checked
	 * @return true if c is instanciable, false otherwise
	 */
	private static boolean canBeInstanciable(Class<?> c) {
		final int notInstantiableClassMask = Modifier.ABSTRACT
				| Modifier.INTERFACE;
		if ((c.getModifiers() & notInstantiableClassMask) != 0
				|| (c.getModifiers() & Modifier.PUBLIC) == 0
				|| c.isLocalClass() || c.isMemberClass() || c.isAnnotation()
				|| c.isAnonymousClass())
			return false;
		return true;
	}

	/**
	 * Tries to load a game plugin from a given set of classes
	 * 
	 * @param classes
	 *            the classes to look through
	 * @return a new GamePlugin if a game has been found, null otherwise
	 * @throws BrokenPluginException
	 *             if the GamePlugin is malformed
	 */
	private GamePlugin loadGamePlugin(Set<Class<?>> classes)
			throws BrokenPluginException {
		Class<? extends Game> gameClass = null;
		Class<? extends Player> playerClass = null;
		Collection<Class<? extends Rule>> rulesClass = new ArrayList<Class<? extends Rule>>();
		Collection<Class<? extends GameInitializer>> initClass = new ArrayList<Class<? extends GameInitializer>>();
		Collection<Class<? extends AIPlayer>> aisClass = new ArrayList<Class<? extends AIPlayer>>();

		for (Class<?> c : classes) {
			if (!canBeInstanciable(c))
				continue;
			if (Game.class.isAssignableFrom(c))
				gameClass = c.asSubclass(Game.class);
			else if (AIPlayer.class.isAssignableFrom(c))
				aisClass.add(c.asSubclass(AIPlayer.class));
			else if (Player.class.isAssignableFrom(c))
				playerClass = c.asSubclass(Player.class);
			else if (Rule.class.isAssignableFrom(c))
				rulesClass.add(c.asSubclass(Rule.class));
			else if (GameInitializer.class.isAssignableFrom(c))
				initClass.add(c.asSubclass(GameInitializer.class));
		}

		if (gameClass == null || playerClass == null)
			return null;

		// GamePlugin construction
		GamePlugin gp;
		try {
			gp = new GamePlugin(gameClass);
			gp.setPlayerType(playerClass);
		} catch (NotInstantiableException e) {
			// if any of these aren't instantiable, the game can't be played
			throw new BrokenPluginException(e.getMessage());
		}
		try {
			for (Class<? extends AIPlayer> c : aisClass)
				gp.addAIType(c);
			for (Class<? extends Rule> c : rulesClass)
				gp.addRule(c);
			for (Class<? extends GameInitializer> c : initClass)
				gp.addInitializer(c);
		} catch (NotInstantiableException e) {
			// if any of these aren't instantiable, the user should be warned
			// yet the game can be played
			e.printStackTrace();
		}
		gamePlugins.put(gp.getPluginName(), gp);
		return gp;
	}

	/**
	 * Tries to load interface plugins from a given set of classes
	 * 
	 * @param classes
	 *            the classes to look through
	 * @return The collection of plugins found
	 */
	private Collection<InterfacePlugin> loadInterfacePlugins(
			Set<Class<?>> classes) {
		Collection<InterfacePlugin> uis = new HashSet<InterfacePlugin>();
		InterfacePlugin tmp;
		for (Class<?> c : classes) {
			if (canBeInstanciable(c) && UserInterface.class.isAssignableFrom(c)) {
				try {
					tmp = new InterfacePlugin(c.asSubclass(UserInterface.class));
					uis.add(tmp);
					uiPlugins.add(tmp);
				} catch (Exception e) {
					// we should inform the user
					e.printStackTrace();
				}
			}
		}
		return uis;
	}

	/**
	 * Tries to load interface module plugins from a given set of classes
	 * 
	 * @param classes
	 *            the classes to look through
	 * @return The collection of plugins found
	 */
	private Collection<InterfaceModulePlugin> loadInterfaceModulePlugins(
			Set<Class<?>> classes) {
		Collection<InterfaceModulePlugin> uims = new HashSet<InterfaceModulePlugin>();
		InterfaceModulePlugin tmp;
		for (Class<?> c : classes) {
			if (canBeInstanciable(c)
					&& InterfaceModule.class.isAssignableFrom(c)) {
				try {
					tmp = new InterfaceModulePlugin(c
							.asSubclass(InterfaceModule.class));
					uims.add(tmp);
					uiPluginModules.add(tmp);
				} catch (NotInstantiableException e) {
					// TODO print down the modules which couldn't be found
					e.printStackTrace();
				}
			}
		}
		return uims;
	}

	/**
	 * Returns the array of the available games names
	 * 
	 * @return the array of the available games names
	 */
	public Collection<String> getGamesName() {
		return new ArrayList<String>(gamePlugins.keySet());
	}

	/**
	 * Returns the array of the available artificial intelligence for a given
	 * game
	 * 
	 * @param gameName
	 *            the name of the game which AIs we want
	 * @return the array of the available artificial intelligence for a given
	 *         game
	 */
	public Collection<String> getAIsName(String gameName) {
		GamePlugin gp = gamePlugins.get(gameName);
		if (gp == null)
			throw new IllegalArgumentException(gameName
					+ " is not a valid game name");
		return new ArrayList<String>(gp.getAINames());
	}

	/**
	 * Returns the list of the name of the available interfaces
	 * 
	 * @return the list of the name of the available interfaces
	 */
	public Collection<String> getInterfacesName() {
		Collection<String> ui = new ArrayList<String>();
		for (InterfacePlugin uip : uiPlugins)
			ui.add(uip.getPluginName());
		return ui;
	}

	/**
	 * Returns a new instance of the game named 'gameName'
	 * 
	 * @param gameName
	 *            the name of the game to instanciate
	 * @return a new instance of the game named 'gameName'
	 */
	public Game getNewGame(String gameName) {
		GamePlugin gp = gamePlugins.get(gameName);
		if (gp == null)
			return null;

		Game game = gp.newGameInstance();
		// adds the interface to the game, so that they become aware of it
		for (UserInterface ui : uiInstances)
			game.addGameListener(ui);

		return game;
	}

	/**
	 * Returns a new instance of a player for the game named 'gameName'
	 * 
	 * @param gameName
	 *            the name of the game to instanciate
	 * @return a new instance of a player for the game named 'gameName'
	 */
	public Player getNewPlayer(String gameName) {
		GamePlugin gp = gamePlugins.get(gameName);
		if (gp == null)
			throw new IllegalArgumentException(gameName
					+ " is not a valid game name");
		return gp.newPlayer();
	}

	/**
	 * Returns a new instance of an AI for the game named 'gameName'
	 * 
	 * @param gameName
	 *            the name of the game to instanciate
	 * @param AIName
	 *            the name of the AI to instanciate
	 * @return a new instance of an AI for the game named 'gameName'
	 */
	public AIPlayer getNewAI(String gameName, String AIName) {
		GamePlugin gp = gamePlugins.get(gameName);
		if (gp == null)
			throw new IllegalArgumentException(gameName
					+ " is not a valid game name");
		return gp.newAIPlayer(AIName);
	}

	public Collection<GameInitializer> getGameInitializers(String gameName) {
		GamePlugin gp = gamePlugins.get(gameName);
		return (gp == null) ? null : new ArrayList<GameInitializer>(gp
				.getInitializers());
	}

	public Collection<Rule> getGameRules(String gameName) {
		GamePlugin gp = gamePlugins.get(gameName);
		return (gp == null) ? null : gp.getRules();
	}

	public UserInterface getNewUserInterface(String uiName) {
		InterfacePlugin founduip = null;
		for (InterfacePlugin uip : uiPlugins) {
			if (uip.getPluginName().equals(uiName))
				founduip = uip;
		}
		if (founduip == null)
			return null;

		UserInterface uiinst = founduip.newInterfaceInstance();
		for (InterfaceModulePlugin imp : uiPluginModules) {
			if (imp.getInterfaceType().isInstance(uiinst))
				try {
					uiinst.addInterfaceModule(imp.newModuleInstance());
				} catch (UnsupportedPluginException e) {
					// TODO we should inform the user the plugin is
					// incoherent with the interface
				}
		}
		uiInstances.add(uiinst);
		return uiinst;
	}

	public Collection<UserInterface> getInterfaceInstances() {
		return new ArrayList<UserInterface>(uiInstances);
	}

}
